/**
*  \file test.c
*
*  \brief   Unit test framework
*
*  \author  Michael J. Hohmann <michael.hohmann@linde-mh.de>
*           Michael Kunkelmann <michael.kunkelmann@linde-mh.de>
*
*
*/

/*===============================================[ private includes  ]================================================*/
#include <stdlib.h> /* exit() */
#include <string.h>
#include <setjmp.h>
#include "test.h"
/*===============================================[ private datatypes ]================================================*/

/** Test Object. */
struct test_obj
{
   U32 random;                   /**< Random value */
   U32 random_seed;              /**< Random seed  */
   U32 passed_count;             /**< Number of successful completed test cases */
   U32 failed_count;             /**< Number of failed test cases */
   U32 test_count;               /**< Total number of performed tests */
   BO  expect_exception;         /**< Flag to indicate if an assert is expected */
   BO  enter_section;            /**< Indicate that a test section was entered, and the close of the section is expected */
   BO  open_test;                /**< Indicate that a test was opened and the closing is expected. */
   U32 debug_before;             /**< Store test number to call debug function before this test will be performed */
   U32 start_time;               /**< Store the time before a test is started */
   U32 test_repeat;              /**< Status flag for repeating the test */
   jmp_buf nirvana;              /**< Jump buffer to catch asserts. */
   enum test_mode mode;          /**< Test mode. This value contains flags. */
};

/** Type enum. */
typedef enum
{
   TYPE_U8,
   TYPE_U16,
   TYPE_U32,
   TYPE_U64,
   TYPE_S8,
   TYPE_S16,
   TYPE_S32,
   TYPE_S64,
   TYPE_F32,
   TYPE_NIL
} test_print_type;

/** base enum. */
typedef enum
{
   NUM_BASE_b,    /* number based on 2 */
   NUM_BASE_o,    /* number based on 8 */
   NUM_BASE_d,    /* number based on 10 */
   NUM_BASE_x,    /* number based on 16 */
   NUM_BASE_f,    /* float number */
   NUM_BASE_max   /* just for max count of enum */
}number_base;


/** base info struct. */
typedef struct BASE_INFO
{
   CH base_char;
   number_base num_base;
   U8 base;
}base_info;

/*===============================================[ private defines   ]================================================*/
/** To get a number from a pointer by type and judge if is negative */
#define get_num(left_num, data, type, neg_flg)  \
   if (*(type *)data < 0)                       \
   {                                            \
      neg_flg = TRUE;                           \
      *((type *)&left_num) = 0 - *(type *)data; \
   }                                            \
   else                                         \
   {                                            \
      *((type *)&left_num) = *(type *)data;     \
   }

/*===============================================[ private variables ]================================================*/
/** static struct instead of local variables. */
struct test_obj me;

/*===============================================[ public variables  ]================================================*/

/*===============================================[ private functions ]================================================*/
static void print_u32( U32 value );
static void print_position_information(CH const * file, U32 line, CH const * function);
static void print_common_start( CH const* info, CH const * label, CH const * description, enum test_case id  );
static void print_timing_information( U32 start_time, U32 stop_time );
static void print_test_result( CH const* info, U32 stop_time, enum test_case id, CH const * label, CH const * description, CH const * file, U32 line, CH const * function);
static void test_inline_case( BO result, enum test_case id, CH const * label, CH const * description, CH const * file, U32 line, CH const * function);
static CH const * test_get_filename(CH const *filename );
static void test_printf(CH const* description, const void *arg);


/*====================================================================================================================*/
void test_auto_open(CH const * description, enum test_mode mode, U32 random_seed )
{
   memset( &me,0,sizeof(me));
   me.random_seed = random_seed;
   me.random = me.random_seed;
   me.mode= mode;
   me.passed_count = 0;
   me.failed_count = 0;
   me.test_count   = 0;
   me.debug_before = U32_MAX;

   me.open_test=TRUE; /* 2 calls of test_auto_close() exptected */

   test_print( "execute( description='" );
   test_print( description );
   test_print( "', random_seed=" );
   print_u32( me.random );
   test_print( ", testcases=" );
   print_u32((U32) test_case_last );
   test_print( ", ticks_per_second=" );
   print_u32( (U32) test_get_ticks_per_second() );
   test_print( ")\n\n" );
}

BO test_auto_close(void)
{
   if( 1 == me.test_repeat)
   {
      me.test_repeat++;
      me.random=me.random_seed;
      me.test_count=0;
      me.passed_count=0;
      me.failed_count=0;

      test_print( "repeat()\n" );
      return TRUE;
   }

   if (me.open_test==TRUE)
   {
      me.open_test=FALSE;
      return TRUE;
   }

   test_print("completed( success=");
   print_u32(me.passed_count);
   test_print(" ,failed=");
   print_u32(me.failed_count);
   test_print(")\n");

   return FALSE;
}

void test_auto_open_section( enum test_case id, CH const * label, CH const * description, CH const * file, U32 line, CH const * function )
{
   me.enter_section=TRUE;

   if( !( test_mode_report_no_section & me.mode) )
   {
      print_common_start( "open", label, description, id );
      print_position_information( file, line, function);
   }
}

BO test_auto_close_section(void)
{
   if (me.enter_section==TRUE)
   {
      me.enter_section=FALSE;
      return 1;
   }

   if( !( test_mode_report_no_section & me.mode) )
   {
      test_print("close( )\n\n");
   }
   return 0;
}

BO test_before_execution( void )
{
   me.start_time=test_get_time(); /* take the time before execution of the testcase */
   return me.test_count==me.debug_before;
}

void test_case( BO result, enum test_case id, CH const * label, CH const * description, CH const * file, U32 line, CH const * function)
{
   test_inline_case( result, id, label, description, file, line, function);
}

void test_info( CH const * description)
{
   test_print("info( description='");
   test_print(description);
   test_print("' )\n" );
}

void test_info_x( CH const * description, void* arg)
{
   test_print("info( description='");
   test_printf(description, arg);
   test_print("' )\n" );
}

void test_rand(void *data, U32 size)
{
   U8 *p=data;

   while(size--)
   {
      me.random = me.random  * 1103515245 + 12345;
      *p=(U8) me.random;
      p++;
   }
}

void test_catch_exception( CH const * description, CH const *file, U32 line, CH const *function )
{
	if( me.expect_exception )
	{
		me.expect_exception=FALSE;
		longjmp( me.nirvana, 1 );
	}
	else
	{
	   test_print("\naborted( 'description='Unexpected ASSERT:");
      test_print(description);
      test_print("', success=");
	   print_u32(me.passed_count);
	   test_print(", failed=");
	   print_u32(me.failed_count);
	   test_print(", file='");
	   test_print(file);
	   test_print("', line=");
	   print_u32( line );
	   test_print(", function='");
	   test_print(function);
	   test_print("')\n");

		exit( 0 );
	}
}

jmp_buf *test_expect_assert()
{
	me.expect_exception=TRUE;
	return &me.nirvana;
}


U8 test_rand_u8( void  )
{
	U8 rv=0;
	test_rand( &rv, sizeof rv );
	return rv;
}

S8 test_rand_s8( void  )
{
	S8 rv=0;
	test_rand( &rv, sizeof rv );
	return rv;
}



U16 test_rand_u16( void  )
{
	U16 rv=0;
	test_rand( &rv, sizeof rv );
	return rv;
}

S16 test_rand_s16( void  )
{
	S16 rv=0;
	test_rand( &rv, sizeof rv );
	return rv;
}

U32 test_rand_u32( void  )
{
	U32 rv=0;
	test_rand( &rv, sizeof rv );
	return rv;
}

S32 test_rand_s32( void  )
{
	S32 rv=0;
	test_rand( &rv, sizeof rv );
	return rv;
}

pal_size_t test_rand_size( void )
{
	pal_size_t rv=0;
	test_rand( &rv, sizeof rv );
	return rv;
}

BO test_rand_bo( void )
{
	BO value=0;
	test_rand( &value, sizeof value );
	return value & 0x01;
}



/*====================================================================================================================*/

/** Print a U32 value.
*
*  \param value Will be printed as string.
*/
static void print_u32( U32 value )
{
   CH buffer[10];
   CH *p = &buffer[9]; /* set p to the last element of the buffer */

   buffer[9]=0; /* set termination zero */

   do
   {
      U8 number;
      number = (U8) (value % 10);
      value /= 10;
      p--;
      *p = (CH)('0'+number);
   }
   while( value );

   test_print(p);
}

static void print_position_information(CH const * file, U32 line, CH const * function)
{
   test_print(", file='");
   test_print( test_get_filename(file));
   test_print(":");
   print_u32( line );
   test_print("'");

   //test_print("', line=");
   //print_u32( line );

   if( ( NIL != function ) && !( test_mode_report_no_function & me.mode ) )
   {
      test_print(", function='");
      test_print(function);
      test_print("'");
   }
   test_print(" )\n");
}

static void print_common_start( CH const* info, CH const * label, CH const * description, enum test_case id  )
{
   test_print(info);
   test_print("( ");

   {
      test_print("label='");
      test_print(label);
      test_print("'");
   }

   if( !(test_mode_report_no_description & me.mode ) )
   {
      test_print(", description='");
      test_print(description);
      test_print("'");
   }

   if( !(test_mode_report_no_id & me.mode ) )
   {
      test_print(",id=");
      print_u32(id);
   }

}


static void print_timing_information( U32 start_time, U32 stop_time )
{
   if( !(test_mode_report_no_timing & me.mode ) )
   {
      test_print(", time=");
      print_u32( start_time );
      test_print(", duration=");
      print_u32( stop_time-start_time );
    }
}

static void print_test_result( CH const* info, U32 stop_time, enum test_case id, CH const * label, CH const * description, CH const * file, U32 line, CH const * function)
{
   print_common_start( info, label, description, id );

   if( !( test_mode_report_no_count & me.mode) )
   {
      test_print(", count=");
      print_u32( me.test_count );
   }
   print_timing_information(me.start_time, stop_time );
   print_position_information( file, line, function);

}

static void test_inline_case( BO result, enum test_case id, CH const * label, CH const * description, CH const * file, U32 line, CH const * function)
{
   U32 stop_time = test_get_time(); /* take the time after execution of the test case */
   CH const* info;

   if(result)
   {
      me.passed_count++;
      info="passed";
   }
   else
   {
      if( 0 != (me.mode & test_mode_repeat_on_error) )
      {
         if( 0 == me.test_repeat) /* only for the first error */
         {
            me.debug_before=me.test_count;
            me.test_repeat=1;
         }
      }
      me.failed_count++;
      info="failed";
   }

   me.test_count++;
   if(( me.mode & test_mode_report_no_passed)&&(TRUE==result))
   {
      /* do nothing */
   }
   else
   {
      print_test_result( info, stop_time, id, label, description, file, line, function);
   }
}

static CH const * test_get_filename(CH const *filename )
{
   CH const *p=filename;
   CH const *begin=filename;
   while( 0!=*p )
   {
      if( ('\\'==*p ) || ( '/'==*p ) )
      {
         p++;
         begin=p;
      }
      else
      {
         p++;
      }
   }
   return begin;
}

static U8 test_print_num(const void *data, test_print_type type, U8 alignment, const base_info* base_info, BO is_fill_needded)
{
   U8 dec_cnt = 0;
   U8 number_len = 0;
   U64 div_base = 1U;
   U64 left_num = 0;
   U8 print_len = 0;
   BO neg_flg = FALSE;
   const static U8 hex_ch[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
   if (data == NIL)
   {
      return 0;
   }
   if((base_info->num_base == NUM_BASE_x) || (base_info->num_base == NUM_BASE_b))
   {
      /* for hex and bit we make sure it to be postive. */
      type = type % 4;
   }
   switch (type)
   {
      case TYPE_S8: get_num(left_num, data, S8, neg_flg); break;
      case TYPE_U8: get_num(left_num, data, U8, neg_flg); break;
      case TYPE_S16: get_num(left_num, data, S16, neg_flg); break;
      case TYPE_U16: get_num(left_num, data, U16, neg_flg); break;
      case TYPE_S32: get_num(left_num, data, S32, neg_flg); break;
      case TYPE_U32: get_num(left_num, data, U32, neg_flg); break;
      case TYPE_S64: get_num(left_num, data, S64, neg_flg); break;
      case TYPE_U64: get_num(left_num, data, U64, neg_flg); break;
      case TYPE_F32: 
         {
            F32 f32_data;
            get_num(f32_data, data, F32, neg_flg); 
            left_num = (U64)(f32_data * 1000000000U);
         }
         break;
      default:
         return 0;
   }

   /* calculate the print length. */
   {
      U64 tmp = left_num;
      if(tmp == 0)
      {
         number_len = 1;
      }
      else
      {
         while (tmp > 0)
         {
            if(number_len > 0)
            {
               div_base *= base_info->base;
            }
            number_len++;
            tmp /= base_info->base;
         }
      }
      if(neg_flg != FALSE)
      {
         number_len ++;
      }
      print_len = alignment;
      if (print_len < number_len)
      {
         print_len = number_len;
      }
   }

   for (dec_cnt = 0; dec_cnt < print_len; dec_cnt++)
   {
      if(dec_cnt + number_len < print_len)
      {
         if(is_fill_needded == FALSE)
         {
            test_nprint(" ", 1);
         }
         else
         {
            test_nprint("0", 1);
         }
      }
      else if ((dec_cnt + number_len == print_len) && (neg_flg != FALSE))
      {
         test_nprint("-", 1);
      }
      else
      {
         if ( type == TYPE_F32 )
         {
            if (dec_cnt == 0 && left_num < 1000000000U)
            {
               test_nprint("0", 1);
            }
            
            if((print_len - dec_cnt) == 9)
            {
               test_nprint(".", 1);
            }
         }
         
         test_nprint((CH *)&hex_ch[left_num / div_base], 1);
         left_num = left_num % div_base;
         div_base = div_base / base_info->base;
      }
   }

   return print_len;
}

#define is_2bytes_euqal(ptr, byte1, byte2) (((ptr)[0] == byte1) && ((ptr)[1] == byte2))
            
static void test_print_arg(CH **read_ptr, const void* data)
{
   const static base_info base_array[] = 
   {
      {'b', NUM_BASE_b, 2},     /* number based on 2 */
      {'o', NUM_BASE_o, 8},     /* number based on 8 */
      {'d', NUM_BASE_d, 10},    /* number based on 10 */
      {'x', NUM_BASE_x, 16},    /* number based on 16 */
      {'f', NUM_BASE_f, 10}     /* float number */
   };
   test_print_type arg_type = TYPE_NIL;
   CH print_len = 0;
   BO is_type_confirmed = FALSE;
   BO is_fill_req_confirmed = FALSE;
   BO is_fill_needded = FALSE;
   while (**read_ptr != '\0')
   {
      switch (**read_ptr)
      {
         case 'c':
         {
            test_nprint((CH*)data, 1);
            *read_ptr = *read_ptr + 1;
            return;
         }
         case 's':
         {
            CH *c_ptr = (CH*)data;
            while(*c_ptr != '\0')
            {
               test_nprint(c_ptr++, 1);
            }
            *read_ptr = *read_ptr + 1;
            return;
         }
         case 'S':
         {
            /* type base on S8 */
            arg_type = TYPE_S8;
            break;
         }
         case 'F':
         case 'f':
         {
            /* type base on U8 */
            if(is_2bytes_euqal(*read_ptr + 1 , '3','2'))
            {
               arg_type = TYPE_F32;
               *read_ptr = *read_ptr + 2;
            }
            break;
         }
         case 'U':
         case 'u':
         {
            /* type base on U8 */
            arg_type = TYPE_U8;
            break;
         }
         case 'o':
         case 'b':
         case 'x':
         case 'd':
         {
            U8 num_base;
            for (num_base = 0; num_base < NUM_BASE_max; num_base++)
            {
               if ( base_array[num_base].base_char == **read_ptr)
               {
                  break;
               }
            }
            
            print_len = test_print_num( data, arg_type, print_len, &base_array[num_base], is_fill_needded);
            *read_ptr = *read_ptr + 1;
            return;
         }
         case '%':
         {
            test_nprint("%", 1);
            *read_ptr = *read_ptr + 1;
            return;
         }
         default:
         {
            if ((**read_ptr >= '0') && (**read_ptr <= '9') && (arg_type != TYPE_NIL))
            {
               if((is_type_confirmed == FALSE) && *(*read_ptr + 1) != '\0')
               {
                  is_type_confirmed = TRUE;
                  /* U16 S16 */
                  if(is_2bytes_euqal(*read_ptr, '1','6'))
                  {
                     arg_type += 1U;
                  }
                  /* U32 S32 */
                  else if(is_2bytes_euqal(*read_ptr, '3','2'))
                  {
                     arg_type += 2U;
                  }                  
                  /* U64 S64 */
                  else if(is_2bytes_euqal(*read_ptr, '6','4'))
                  {
                     arg_type += 3U;
                  }
                  else if(**read_ptr == '8')
                  {
                     *read_ptr = *read_ptr + 1;
                     continue;
                  }
                  else
                  {
                     test_nprint(*read_ptr -2 , 3);
                     *read_ptr = *read_ptr + 1;
                     /* parse error, return */
                     return;
                  }
                  *read_ptr = *read_ptr + 1;
               }
               else
               {
                  if(is_type_confirmed != FALSE)
                  {
                     if((**read_ptr == '0') && (is_fill_req_confirmed == FALSE))
                     {
                        is_fill_needded = TRUE;
                     }
                     else
                     {
                        print_len = print_len * 10 + (**read_ptr - '0');
                     }
                     is_fill_req_confirmed = TRUE;
                  }
               }
            }
            else
            {
               test_nprint(*read_ptr -1 , 2);
               return;
            }
            break;
         }
      }
      *read_ptr = *read_ptr + 1;
   }
}

/** Function to print out test results with parameter.
*
*  The implemented function should be able to transfer the passed contend to a system where the information
*  can be collected to a test protocol.
*  On a PC-System this is typical a printf to the console.
*  On an embedded it could be a transmission via CAN-Bus.
*
*  \param the arguments are the same as printf buf only 1 parameter is supposed.
*/
static void test_printf(CH const* description, const void *arg)
{
   CH* read_ptr = (CH*) description;
   CH* print_const_ptr = (CH*) description;
   size_t len_const = 0;
   while(*read_ptr != '\0')
   {
      if(*read_ptr == '%')
      {
         /* print the string before % */
         test_nprint(print_const_ptr, len_const);
         len_const = 0;
         /* print the argument */
         read_ptr++;
         if(arg != NIL)
         {
            test_print_arg(&read_ptr, arg); 
         }
         print_const_ptr = read_ptr;
      }
      else
      {
         if(*read_ptr == '\n')
         {
            /* print the string before \n */
            test_nprint(print_const_ptr, len_const);
            len_const = 0;
            test_nprint("\\n",2);
            if(print_const_ptr == read_ptr)
            {
               print_const_ptr ++;
            }
         }
         else
         {
            len_const ++;
         }
         read_ptr++;
      }
   }
   if(len_const > 0)
   {
      /* print the left string */
      test_nprint(print_const_ptr, len_const);
   }
}

/***********************************************************************************************************************
*                             Copyright 2011 - 2017 Linde Material Handling. All rights reserved.                      *
***********************************************************************************************************************/
